前一天我們已經練習了 RecyclerView 與 Room,讓 Android App 可以把資料儲存在手機本機。
到了 Day 26,資料準備「上雲端」了。
今天要正式接觸 Firebase,並使用 Firebase Realtime Database,讓 Android App 不再只有單機資料,而是可以把資料寫進雲端資料庫。
今天的重點:
Android App → Firebase SDK → Realtime Database → 雲端資料
參考資料:Firebase 官方網站
前面使用 Room 時,資料基本上是:
Android App
│
▼
Room
│
▼
SQLite
│
▼
手機本機
這種方式很適合:
但假設今天做的是:
智慧水杯墊
飲料點餐系統
會員系統
IoT 裝置
多人共用資料
如果資料只存在手機裡,就會遇到一個問題:
換一支手機,原本的資料怎麼辦?
因此我們開始需要「雲端資料庫」。
Firebase 是 Google 提供的 App 開發平台,其中包含 Authentication、Realtime Database、Cloud Firestore、Cloud Storage、Cloud Messaging 等服務。
參考資料:Firebase 官方文件
今天第一件事情,就是進入 Firebase Console。

從畫面可以看到 Firebase Console 中已經存在數個 Firebase 專案,例如:
new-firebase-2024
FirebaseAPP
FabTest
BitLinkWeb
也可以從:
建立新的 Firebase 專案
開始建立新的 Firebase Project。
Firebase Console:
Firebase 提供不只一種資料庫。
今天我們先使用:
它是一個雲端 NoSQL 資料庫。
與傳統 SQL 資料庫不同,它的資料結構比較接近:
JSON Tree
例如今天程式建立的資料,可以想成:
class_2
│
├── 1
│ ├── name : AAAA
│ ├── email : aaa@gmail.com
│ ├── phone : 11111
│ └── sex : Female
│
├── 2
│ ├── name : BBBB
│ ├── email : bbb@gmail.com
│ └── phone : 22222
│
├── 3
│ ├── name : CCCC
│ ├── email : ccc@gmail.com
│ └── phone : 33333
│
└── 4
├── name : Mary
├── email : mary@gmail.com
└── phone : 44444
這也是今天程式碼最重要的觀念之一。
參考資料:Firebase Realtime Database 官方文件
今天的 Firebase 範例首先宣告:
private lateinit var dataRef: DatabaseReference
private lateinit var fbControl: FirebaseDatabase
接著在 onCreate() 中:
fbControl = FirebaseDatabase.getInstance()
Log.d("main", "fbControl = $fbControl")
FirebaseDatabase.getInstance() 可以取得 Firebase Realtime Database 的實例。
所以可以先把它理解成:
FirebaseDatabase
│
▼
取得 Firebase Realtime Database
│
▼
準備讀寫雲端資料
這是 Android App 與 Firebase Database 建立操作入口的重要一步。
參考資料:Firebase Android Realtime Database 使用說明
接下來:
dataRef = fbControl.getReference().child("class_2")
這裡取得的是:
DatabaseReference
可以把 DatabaseReference 想成:
「Firebase 資料庫裡某一個位置的參考位置。」
例如:
Root
│
└── class_2
程式:
fbControl.getReference()
代表資料庫根節點。
再接:
.child("class_2")
就會指向:
/class_2
因此:
dataRef
現在就是指向 class_2 的 DatabaseReference。
今天最直觀的寫法是:
dataRef.child("1").child("name").setValue("AAAA")
dataRef.child("1").child("email").setValue("aaa@gmail.com")
dataRef.child("1").child("phone").setValue("11111")
拆開看就非常容易理解。
第一行:
dataRef.child("1").child("name").setValue("AAAA")
相當於:
class_2
│
└── 1
│
└── name
│
└── AAAA
第二行:
dataRef.child("1").child("email").setValue("aaa@gmail.com")
結果:
class_2
│
└── 1
├── name : AAAA
└── email : aaa@gmail.com
第三行再加入:
dataRef.child("1").child("phone").setValue("11111")
最後就形成:
class_2
│
└── 1
├── name : AAAA
├── email : aaa@gmail.com
└── phone : 11111
也就是說:
child()
負責往下一層節點走。
而:
setValue()
負責把值寫進該位置。
今天程式接著建立三組資料:
dataRef.child("1").child("name").setValue("AAAA")
dataRef.child("1").child("email").setValue("aaa@gmail.com")
dataRef.child("1").child("phone").setValue("11111")
dataRef.child("2").child("name").setValue("BBBB")
dataRef.child("2").child("email").setValue("bbb@gmail.com")
dataRef.child("2").child("phone").setValue("22222")
dataRef.child("3").child("name").setValue("CCCC")
dataRef.child("3").child("email").setValue("ccc@gmail.com")
dataRef.child("3").child("phone").setValue("33333")
這樣 Firebase 的結構就會變成:
class_2
│
├── 1
│ ├── name : AAAA
│ ├── email : aaa@gmail.com
│ └── phone : 11111
│
├── 2
│ ├── name : BBBB
│ ├── email : bbb@gmail.com
│ └── phone : 22222
│
└── 3
├── name : CCCC
├── email : ccc@gmail.com
└── phone : 33333
這就是 NoSQL 樹狀資料結構非常直觀的地方。
接下來有一行很值得注意:
dataRef.child("1").child("sex").setValue("Female")
原本:
1
├── name
├── email
└── phone
現在又增加:
1
├── name
├── email
├── phone
└── sex
也就是:
1
├── name : AAAA
├── email : aaa@gmail.com
├── phone : 11111
└── sex : Female
從這裡可以開始感受到 Firebase Realtime Database 與傳統關聯式資料庫操作方式的差異。
參考資料:Firebase Realtime Database 資料結構
前面的寫法是:
.child("name").setValue(...)
.child("email").setValue(...)
.child("phone").setValue(...)
但是如果一筆資料有很多欄位,一行一行寫會比較麻煩。
所以今天又用了另一種方法:
val data = mutableMapOf<String, String>()
data["name"] = "Mary"
data["email"] = "mary@gmail.com"
data["phone"] = "44444"
dataRef.child("4").setValue(data)
這次不是逐欄寫入,而是先建立:
Map
內容:
name → Mary
email → mary@gmail.com
phone → 44444
最後:
dataRef.child("4").setValue(data)
一次把整個 Map 寫進:
class_2/4
結果就是:
4
├── name : Mary
├── email : mary@gmail.com
└── phone : 44444
這比逐欄呼叫 setValue() 更接近實際 App 的資料處理方式。
參考資料:Firebase 寫入資料官方說明
其實今天的內容並不是突然跳到另一個完全無關的東西。
前面的 RecyclerView 練習已經把資料整理成:
MutableList<MutableMap<String, Any>>
例如飲料資料:
data.put("name", nameArray[i])
data.put("price", priceArray[i])
data.put("pic", picArray.getResourceId(i, R.drawable.blacktea))
data.put("qty", 0)
再透過 RecyclerView 顯示:
飲料名稱
價格
圖片
數量
而 Adapter 中還加入:
+ → 數量 +1
- → 數量 -1
最後結帳時:
sum += qty * price
把選購項目及總價整理出來。
因此現在開始學 Firebase,下一步其實就非常清楚:
RecyclerView
│
▼
使用者選商品
│
▼
修改 qty
│
▼
按下結帳
│
▼
產生訂單資料
│
▼
Firebase Realtime Database
參考資料:RecyclerView 官方文件
今天的資料裡還有另一個 RecyclerView 範例。
先建立:
data class Flower(
val name: String,
val imageResourceId: Int
)
這比:
MutableMap<String, Any>
更具有明確的資料型別。
接著 FlowerAdapter 接收:
private val flowerList: List<Flower>
並且在:
override fun onBindViewHolder(
holder: FlowerViewHolder,
position: Int
)
取得:
val flower = flowerList[position]
然後:
holder.nameTextView.text = flower.name
holder.picImageView.setImageResource(flower.imageResourceId)
最後加入點擊 Callback:
holder.itemView.setOnClickListener {
onItemClick(flower)
}
這就是很典型的:
Data Class
↓
List<Flower>
↓
RecyclerView.Adapter
↓
ViewHolder
↓
畫面
架構。
另一份範例則使用:
val adapterClick: (MutableMap<String, Any>) -> Unit
在 Adapter 裡:
holder.itemView.setOnClickListener {
Log.d("main", "it")
adapterClick(data)
}
然後回到 Activity:
val adapter = MyAdapter(this, dataList) { itemData ->
Log.d("main", "main click = $itemData")
textViewName.text = itemData.get("name").toString()
imageViewPic.setImageResource(itemData.get("pic") as Int)
}
這裡其實就是之前學過的 Lambda 真正開始運用在 Android UI。
流程:
使用者點 RecyclerView Item
│
▼
Adapter
│
▼
adapterClick()
│
▼
MainActivity
│
┌─────┴─────┐
▼ ▼
修改文字 修改圖片
這也是為什麼前面學 Kotlin Lambda 並不是只為了考語法,進到 Android 後會一直看到它。
參考資料:Kotlin Lambda 官方文件
今天的資料中還保留了之前的飲料點餐範例。
MainActivity 選擇飲料後:
val intent = Intent(this, SelectActivity::class.java)
intent.putExtra("selected_drink", selectedDrinkName)
launcher.launch(intent)
進入 SelectActivity。
第二個 Activity 接收:
val selectedDrink = intent.getStringExtra("selected_drink")
edDrink.setText(selectedDrink)
使用者設定:
飲料
數量
甜度
冰塊
再用 Bundle 回傳:
val intent = Intent()
val bundle = Bundle()
bundle.putString("drink", drink)
bundle.putString("quantity", quantity)
bundle.putString("sugar", sugar)
bundle.putString("ice", ice)
intent.putExtras(bundle)
setResult(Activity.RESULT_OK, intent)
finish()
MainActivity 再透過:
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
)
接收結果並計算:
val quantity = quantityStr.toIntOrNull() ?: 0
val total = quantity * selectedDrinkPrice
最後顯示:
飲料名稱
數量
甜度
冰塊
總價
參考資料:Android Activity Result API
一路寫到今天,會發現之前學過的東西其實開始連起來了:
Kotlin
│
├── Array
├── List
├── Map
├── Data Class
└── Lambda
│
▼
Android
│
├── Activity
├── Intent
├── Bundle
├── ListView
└── RecyclerView
│
▼
資料處理
│
├── Room
└── SQLite
│
▼
今天
│
▼
Firebase
│
▼
Realtime Database
│
▼
Cloud
前面看似一個一個獨立的小練習,到了 Firebase 開始可以慢慢組成真正的 App。
把今天最重要的程式濃縮起來,就是:
// 取得 Firebase Realtime Database
fbControl = FirebaseDatabase.getInstance()
// 取得 class_2 節點
dataRef = fbControl.getReference().child("class_2")
// 寫入第一筆資料
dataRef.child("1").child("name").setValue("AAAA")
dataRef.child("1").child("email").setValue("aaa@gmail.com")
dataRef.child("1").child("phone").setValue("11111")
// 寫入第二筆資料
dataRef.child("2").child("name").setValue("BBBB")
dataRef.child("2").child("email").setValue("bbb@gmail.com")
dataRef.child("2").child("phone").setValue("22222")
// 寫入第三筆資料
dataRef.child("3").child("name").setValue("CCCC")
dataRef.child("3").child("email").setValue("ccc@gmail.com")
dataRef.child("3").child("phone").setValue("33333")
// 在既有節點增加 sex
dataRef.child("1").child("sex").setValue("Female")
// 使用 Map 建立第四筆資料
val data = mutableMapOf<String, String>()
data["name"] = "Mary"
data["email"] = "mary@gmail.com"
data["phone"] = "44444"
// 一次寫入整筆資料
dataRef.child("4").setValue(data)
這段先掌握四個關鍵字:
FirebaseDatabase
DatabaseReference
child()
setValue()
Firebase Realtime Database 最基本的資料寫入就已經建立起來了。
參考資料:Firebase Realtime Database Android 讀寫資料
經過 Day 25 與 Day 26,剛好可以做一個簡單比較:
| 比較 | Room | Firebase Realtime Database |
|---|---|---|
| 資料位置 | 手機本機 | 雲端 |
| 底層概念 | SQLite | NoSQL JSON Tree |
| 網路 | 可完全離線 | 主要用於同步雲端資料 |
| 多裝置 | 需另外設計同步 | 適合跨裝置同步 |
| Android 整合 | Jetpack | Firebase SDK |
| 常見用途 | 本機資料、快取 | 即時資料、多人共享、IoT |
所以實際專案不一定是:
Room OR Firebase
也可能是:
Android App
/ \
/ \
Room Firebase
│ │
本機資料 雲端資料
兩者其實可以一起使用。
參考資料:Android Room 官方文件、Firebase Realtime Database
最後把 Day 26 整理成一張流程:
┌─────────────────────┐
│ Android App │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ FirebaseDatabase │
│ .getInstance() │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ DatabaseReference │
│ child("class_2") │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ child("1") │
│ child("name") │
│ setValue("AAAA") │
└──────────┬──────────┘
│
▼
☁️ Firebase Realtime Database
│
▼
class_2
├── 1
│ ├── name
│ ├── email
│ ├── phone
│ └── sex
├── 2
├── 3
└── 4
從今天開始,我們的 Android App 已經不再侷限於手機本機。
資料正式上雲端了。
參考資料:Firebase Realtime Database 官方文件
今天主要完成三個階段的整合:
第一階段:RecyclerView
資料 → Adapter → ViewHolder → 畫面
第二階段:Intent
MainActivity
↓
SelectActivity
↓
Bundle
↓
MainActivity
第三階段:Firebase
Android
↓
Firebase SDK
↓
DatabaseReference
↓
setValue()
↓
Realtime Database
其中今天真正需要記住的 Firebase 基礎語法就是:
FirebaseDatabase.getInstance()
getReference()
child()
setValue()
前一天我們讓資料留在手機裡,今天則跨出了重要的一步:
從 Local Database 走向 Cloud Database。
接下來就可以繼續挑戰 Firebase 資料讀取、修改、刪除與即時監聽,讓目前只有「寫入」能力的 Android App,真正完成雲端 CRUD。